package mosync.mobilelualiveserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class Server extends Thread
{
	private boolean mRunning = false;
	private ArrayList<ClientConnection> mClientConnections;
	private ClientSocketAcceptor mClientAcceptor;
	private ArrayList<EditorConnection> mEditorConnections;
	private EditorSocketAcceptor mEditorAcceptor;
	
	public Server()
	{
		mRunning = false;
		mClientConnections = new ArrayList<ClientConnection>();
		mClientAcceptor = new ClientSocketAcceptor(55555, this);
		
		mEditorConnections = new ArrayList<EditorConnection>();
		mEditorAcceptor = new EditorSocketAcceptor(55556, this);
	}
	
	public void startServer()
	{
		if (!mRunning)
		{
			// Start my thread.
			start();
			mRunning = true;
			
			// Start acceptor threads.
			mClientAcceptor.start();
			mEditorAcceptor.start();
		}
	}

	private void stopServer()
	{
		// I am not running any longer.
		mRunning = false;
		
		// Stop acceptors.
		mClientAcceptor.close();
		mEditorAcceptor.close();
	}
	
	public void run()
	{
		worker();
	}

	private void worker()
	{
		while (mRunning)
		{
			// Wait for message (from client or editor connections).
			Message message = waitForMessage();
			
			Log.i("Server got message: " + message.getMessage());
			
			if ("ClientConnectionCreated".equals(message.getMessage()))
			{
				// Get connection object.
				ClientConnection connection = (ClientConnection) message.getObject();
				
				// Add connection.
				Log.i("Adding client connection: " + connection);
				mClientConnections.add(connection);
				
				// Start connection thread.
				connection.start();
			}
			else if ("ClientConnectionClosed".equals(message.getMessage()))
			{
				mClientConnections.remove(message.getObject());
			}
			if ("EditorConnectionCreated".equals(message.getMessage()))
			{
				// Get connection object.
				EditorConnection connection = (EditorConnection) message.getObject();
				
				// Add connection.
				Log.i("Adding editor connection: " + connection);
				mEditorConnections.add(connection);
				
				// Start connection thread.
				connection.start();
			}
			else if ("EditorConnectionClosed".equals(message.getMessage()))
			{
				mEditorConnections.remove(message.getObject());
			}
			else if ("CommandRunProgram".equals(message.getMessage()))
			{
				Log.i("Sending CommandRunProgram to clients");
				sendMessageToClients(message);
			}
			else if ("CommandRunSelection".equals(message.getMessage()))
			{
				Log.i("Sending CommandRunSelection to clients");
				sendMessageToClients(message);
			}
			else if ("MessageFromClient".equals(message.getMessage()))
			{
				Log.i("MessageFromClient: " + message.getObject());
				sendMessageToEditors(message);
			}
			else if ("CommandServerStop".equals(message.getMessage()))
			{
				stopServer();
			}
			else if("ServerAddressReceived".equals(message.getMessage())) 
			{
				Log.i("ServerAddressReceived: " + message.getObject());
				sendMessageToEditors(message);
			}
		}
	}
	
	private void sendMessageToClients(Message message)
	{
		for (ClientConnection connection : mClientConnections)
		{
			Log.i("Sending Message: " + message + " to client connection: " + connection);
			connection.postMessage(message);
		}
	}

	private void sendMessageToEditors(Message message)
	{
		for (EditorConnection connection : mEditorConnections)
		{
			Log.i("Sending Message: " + message + " to editor connection: " + connection);
			connection.postMessage(message);
		}
	}
	
	/**
	 * This is the server socket that accepts client 
	 * connections, that is, mobile devices listening
	 * for commands to execute.
	 */
	static class ClientSocketAcceptor extends Thread
	{
		private int mPort;
		private Server mServer;
		private ServerSocket mServerSocket;
		
		public ClientSocketAcceptor(int port, Server server)
		{
			mPort = port;
			mServer = server;
		}
		
		public void run()
		{
			try 
			{
				worker();
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}
		}

		/**
		 * Call close to terminate the thread.
		 */
		public void close()
		{
			if (null != mServerSocket)
			{
				try 
				{
					mServerSocket.close();
				} 
				catch (IOException e) 
				{
					e.printStackTrace();
				}
			}
		}
		
		public void worker() throws IOException
		{
			mServerSocket = new ServerSocket(mPort);
			
			while (true)
			{
				Log.i("Waiting for client connection");
				Socket socket = mServerSocket.accept();
				Log.i("Client connection accepted");
				ClientConnection clientConnection = 
					new ClientConnection(socket, mServer);
				mServer.postMessage(
					new Message("ClientConnectionCreated", clientConnection));
			}
		}
	}
	
	/**
	 * This is the server socket that accepts editor connections.
	 */
	static class EditorSocketAcceptor extends Thread
	{
		private int mPort;
		private Server mServer;
		private ServerSocket mServerSocket;
		
		public EditorSocketAcceptor(int port, Server server)
		{
			mPort = port;
			mServer = server;
		}
		
		public void run()
		{
			try 
			{
				worker();
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}
		}

		/**
		 * Call close to terminate the thread.
		 */
		public void close()
		{
			if (null != mServerSocket)
			{
				try 
				{
					mServerSocket.close();
				} 
				catch (IOException e) 
				{
					e.printStackTrace();
				}
			}
		}
		
		public void worker() throws IOException
		{
			mServerSocket = new ServerSocket(mPort);
			
			while (true)
			{
				Log.i("Waiting for editor connection");
				Socket socket = mServerSocket.accept();
				Log.i("Editor connection accepted");
				EditorConnection editorConnection = 
					new EditorConnection(socket, mServer);
				mServer.postMessage(
					new Message("EditorConnectionCreated", editorConnection));
			}
		}
	}
	
	static class ClientConnection extends MessageThread
	{
		private Socket mSocket;
		private Server mServer;
		private boolean mRunning;

		public ClientConnection(Socket socket, Server server)
		{
			mSocket = socket;
			mServer = server;
			mRunning = true;
		}
		
		public void run()
		{
			try 
			{
				// Start communication.
				worker();
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}
			finally
			{
				// Post connection closed message.
				mServer.postMessage(
					new Message("ClientConnectionClosed", this));
			}
		}
		
		public void worker() throws IOException
		{
			OutputStream out = mSocket.getOutputStream();
			InputStream in = mSocket.getInputStream();
			
			while (mRunning)
			{
				// Wait for message.
				Log.i("Waiting for message in client connection: " + this);
				Message message = waitForMessage();
				Log.i("ClientConnectionMessage: " + message.getMessage());
				
				// Send request to client.
				// First comes an int with length of string, then the string data as an byte string.
				String string = message.getObject().toString();
				byte[] byteString = string.getBytes("ISO-8859-1");
				int dataLength = byteString.length;
				
				// Assume little endian on client.
				int b1 = (dataLength) & 0xFF;
				int b2 = (dataLength >> 8) & 0xFF;
				int b3 = (dataLength >> 16) & 0xFF;
				int b4 = (dataLength >> 24) & 0xFF;
				Log.i(
					"dataLength: " + dataLength + 
					" b1: " + b1 + 
					" b2: " + b2 + 
					" b3: " + b3 + 
					" b4: " + b4);
				out.write(b1);
				out.write(b2);
				out.write(b3);
				out.write(b4);
				out.write(byteString);
				out.flush();
				
				Log.i("string: " + string);
				Log.i("byteString: " + byteString);
				
				// Wait for result.
				int bufSize = 2048;
				byte[] dataBuffer = new byte[bufSize];
				int numBytesRead = 0;
				
				// Read data.
				numBytesRead += in.read(dataBuffer); //, 0, bufSize - numRead);
				
				// Make sure that the data length bytes are read.
				while (numBytesRead < 4)
				{
					numBytesRead += in.read(
						dataBuffer,
						numBytesRead, 
						bufSize - numBytesRead);
				}
				
				// Get length of data.
				int i1 = dataBuffer[0];
				int i2 = dataBuffer[1];
				int i3 = dataBuffer[2];
				int i4 = dataBuffer[3];
				dataLength = 
					(i1) |
					(i2 << 8) |
					(i3 << 16) |
					(i4 << 24);

				Log.i(
					"dataLength: " + dataLength + 
					" i1: " + i1 + 
					" i2: " + i2 + 
					" i3: " + i3 + 
					" i4: " + i4);
				
				// Read remaining bytes.
				while (numBytesRead < 4 + dataLength)
				{
					numBytesRead += in.read(
						dataBuffer, 
						numBytesRead, 
						bufSize - numBytesRead);
				}
				
				String data = new String(dataBuffer, 4, dataLength, "ISO-8859-1");
				
				Log.i(
					"numRead: " + numBytesRead + 
					" data: " + data);
				
				// Post result message to server.
				mServer.postMessage(new Message("MessageFromClient", data));
			}
		}
	}
	
	static class EditorConnection extends MessageThread
	{
		private Socket mSocket;
		private Server mServer;
		private boolean mRunning;

		public EditorConnection(Socket socket, Server server)
		{
			mSocket = socket;
			mServer = server;
			mRunning = true;
		}
		
		public void run()
		{
			try 
			{
				// Start communication.
				startReader();
				startWriter();
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}
			finally
			{
				// Post connection closed message.
				mServer.postMessage(
					new Message("EditorConnectionClosed", this));
			}
		}
		
		public void worker() throws IOException
		{
			OutputStream out = mSocket.getOutputStream();
		    ObjectOutputStream objectOutStream = new ObjectOutputStream(out);
			InputStream in = mSocket.getInputStream();
		    ObjectInputStream objectInStream = new ObjectInputStream(in);
			
			while (mRunning)
			{
				try
				{
					// Listen for commands from the editor. Commands
					// are sent as serialized Java objects of type Message.
					Log.i("Waiting for request in editor connection: " + this);
				    Message request = (Message) objectInStream.readObject();
					Log.i("EditorConnectionMessage: " + request.getMessage());
					
					// Post request message to server.
					mServer.postMessage(request);
					
					// Wait for response from server. We should always get a 
					// response message. Nevertheless, this is a nasty 
					// synchronous assumption, would be better to make this 
					// totally asynchronous and have readers and writers as 
					// separate threads.
					Log.i("Waiting for response in editor connection: " + this);
					Message response = waitForMessage();
					Log.i("EditorConnectionMessage: " + response.getMessage());
					
					// Send response to editor.
					objectOutStream.writeObject(response);
				}
				catch (Exception ex) 
				{
					ex.printStackTrace();
				}
			}
		}
	}
}


